//
// Created by LiuYou on 2021/8/16.
//

#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <cerrno>


static int setFdNonblocking(int fd) {
    int oldOption = fcntl( fd, F_GETFL );
    // 疑惑解开, 将文件描述符设置为非阻塞, 使用的是宏 O_NONBLOCK。
    int newOption = oldOption | O_NONBLOCK;
    fcntl( fd, F_SETFL, newOption );
    return oldOption;
}

static void addEventToEpoll(int fd, int epollFd, bool enableET) {
    epoll_event epollEvent{};
    epollEvent.data.fd = fd;
    epollEvent.events = EPOLLIN;
    if (enableET) {
        epollEvent.events |= EPOLLET;
    }
    /*
     * 既然添加感兴趣监听的事件到epoll了, 那么epoll监听的一定是非阻塞的文件描述符。
     * 所以, 这里要将将要监听的fd设置为非阻塞。
     */
    setFdNonblocking( fd );
    epoll_ctl( epollFd, EPOLL_CTL_ADD, fd, &epollEvent );
}

static int getBufferSize() {
    static int BUFFER_SIZE = 10;
    return BUFFER_SIZE;
}

static void lt(int listenFd, int epollFd, const epoll_event* epollEvents, int readyFds) {
    for (int i = 0; i < readyFds; ++i) {
        if (epollEvents[i].data.fd == listenFd) {
            sockaddr_in remoteAddress{};
            socklen_t socklen = sizeof( remoteAddress );
            // 如果关闭 connectFd?
            // 答案在下面的 recv()调用那里。
            int connectFd = accept( epollEvents[i].data.fd, (sockaddr*) &remoteAddress, &socklen );
            if (connectFd == -1) {
                perror( "accept()" );
//                break;
                // 这里要注意: 一般服务器程序, 比较少break。我觉得, 不能一个
                // 文件描述符上的操作出错了就直接exit()或者break循环, 而是报错并且
                // 继续执行程序(因为服务器程序是不能轻易就停止的。)或者是报错后continue循环
                // 继续执行其它逻辑。
                break;
            }
            // 对connectFd禁用et模式。
            addEventToEpoll( connectFd, epollFd, false );
        } else if (epollEvents[i].events & EPOLLIN) {
            // lt模式
            printf( "event trigger once. (事件触发一次)\n" );
            char buffer[getBufferSize()];
            memset( buffer, '\0', sizeof( buffer ));
            ssize_t rcvBytes = recv( epollEvents[i].data.fd, buffer, getBufferSize() - 1, 0 );
            // 缺少了对 recv()返回值的判断, 现在补上。
            /*
             * todo 调试笔记: connectFd的关闭就在这里。
             *  telnet的关闭(即客户端的断开连接)也是作为一个 EPOLLIN事件来处理的。此时 recv()调用接受到的字节数是0也就是通信
             *  对方已经关闭连接了。所以, 在这个 rcvBytes == 0判断中关闭了 connectFd。
             */
            if (rcvBytes == 0) {
                close( epollEvents[i].data.fd );
                break;
            } else if (rcvBytes == -1) {
                close( epollEvents[i].data.fd );
                perror( "recv()" );
                break;
            }
            // 如何将上一条if语句的remoteAddress共享到这一条else if语句中?
//                    fprintf(stdout,"从 %s:%d 接受到了 %d 字节的数据, 数据为: %s。\n",
//                            )

            fprintf( stdout, "接受到了 %zd 字节的数据, 数据为: %s\n", rcvBytes, buffer );
        }
    }
}

static void et(int listenFd, int epollFd, const epoll_event* epollEvents, int readyFds) {
    for (int i = 0; i < readyFds; ++i) {
        if (epollEvents[i].data.fd == listenFd) {
            sockaddr_in remoteAddress{};
            socklen_t socklen = sizeof( remoteAddress );
            int connectFd = accept( epollEvents[i].data.fd, (sockaddr*) &remoteAddress, &socklen );
            if (connectFd == -1) {
                perror( "accept()" );
                //                break;
                // 这里要注意: 一般服务器程序, 比较少break。我觉得, 不能一个
                // 文件描述符上的操作出错了就直接exit()或者break循环, 而是报错并且
                // 继续执行程序(因为服务器程序是不能轻易就停止的。)或者是报错后continue循环
                // 继续执行其它逻辑。
                // 但是, 这里要补充一下, 如果在while循环中处理时, continue后, 这个循环根本
                // 没有了其它逻辑要处理(即while循环处理的逻辑结束了), 那就直接 break 出来
                // 这个循环。不必在这个循环中一直等。所以, break在这种情况下很常用。
                break;
            }
            // 对connectFd开启et模式。
            addEventToEpoll( connectFd, epollFd, true );
        } else if (epollEvents[i].events & EPOLLIN) {
            // et模式
            printf( "event trigger once. (事件触发一次)\n" );
            while (true) {
                char buffer[getBufferSize()];
                memset( buffer, '\0', sizeof( buffer ));
                ssize_t rcvBytes = recv( epollEvents[i].data.fd, buffer, getBufferSize() - 1, 0 );
                // 缺少了对 recv()返回值的判断, 现在补上。
                if (rcvBytes == 0) {
                    close( epollEvents[i].data.fd );
                    break;
                } else if (rcvBytes < 0) {
                    if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
                        printf( "读完了connectFd中的内容, connectFd现在不是就绪态了。\n" );
                        break;
                    }
                    close( epollEvents[i].data.fd );
                    perror( "recv()" );
                    break;
                } else {
                    fprintf( stdout, "接受到了 %zd 字节的数据, 数据为: %s\n", rcvBytes, buffer );
                }
            }
        }
    }
}

int main(int argc, char** argv) {
    if (argc < 3) {
        fprintf( stderr, "用法: %s ip port\n", basename( argv[0] ));
    }
    char* ip = argv[1];
    int port = atoi( argv[2] );

    int listenFd = socket( AF_INET, SOCK_STREAM, 0 );
    if (listenFd == -1) {
        perror( "socket()" );
        exit( 1 );
    }

    // 处理异常终止, 端口没有立刻释放的问题。这样就可以下一次继续bind()这个端口。
    int optVal = 1;
    if (setsockopt( listenFd, SOL_SOCKET, SO_REUSEADDR, &optVal, sizeof( optVal )) == -1) {
        perror( "setsockopt()" );
        exit( 1 );
    }

    sockaddr_in localAddress{};
    localAddress.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &localAddress.sin_addr );
    localAddress.sin_port = htons( port );
    int ret = bind( listenFd, (sockaddr*) &localAddress, sizeof( localAddress ));
    if (ret == -1) {
        perror( "bind()" );
        close( listenFd );
        exit( 1 );
    }

    ret = listen( listenFd, 5 );
    if (ret == -1) {
        perror( "listen()" );
        close( listenFd );
        exit( 1 );
    }


    int epollFd = epoll_create( 10 );

    addEventToEpoll( listenFd, epollFd, false );

    epoll_event epollEvents[1024]{};

    while (true) {
        // todo 调试笔记: 我通过调试得知 telnet也就是客户端退出后, 也是属于一个 EPOLLIN 事件。会被epoll_waitI()监听到。
        int readyFds = epoll_wait( epollFd, epollEvents, 1024, -1 );
        if (readyFds == -1) {
            perror( "epoll_wait()" );
            break;
        } else {
//            lt( listenFd, epollFd, epollEvents, readyFds );
            // 注意: 每个使用et模式的文件描述符都必须是非阻塞的。
            et( listenFd, epollFd, epollEvents, readyFds );
        }

    }

    /*
     * todo 调试笔记: 同时我知道了, 服务器程序一些内容就是在服务器运行期间, 这些资源就一直被占用。直到服务器停止了,这些资源才释放。
     *  比如: listenFd等或者其它(现在还没有接触到);
     *  而有一些资源是有用户连接才会分配,
     *  比如: connectFd, 但是这样的资源要在客户退出(即不与服务器连接后), 服务器自己将这样的资源释放掉。
     */
    close( listenFd );
    exit( 0 );
}